using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Music.Commands;
using NzbDrone.Core.Music.Events;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NzbDrone.Core.ImportLists.Exclusions;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Core.History;

namespace NzbDrone.Core.Music
{
    public class RefreshArtistService : RefreshEntityServiceBase<Artist, Album>, IExecute<RefreshArtistCommand>
    {
        private readonly IProvideArtistInfo _artistInfo;
        private readonly IArtistService _artistService;
        private readonly IAlbumService _albumService;
        private readonly IRefreshAlbumService _refreshAlbumService;
        private readonly IEventAggregator _eventAggregator;
        private readonly IMediaFileService _mediaFileService;
        private readonly IHistoryService _historyService;
        private readonly IDiskScanService _diskScanService;
        private readonly ICheckIfArtistShouldBeRefreshed _checkIfArtistShouldBeRefreshed;
        private readonly IConfigService _configService;
        private readonly IImportListExclusionService _importListExclusionService;
        private readonly Logger _logger;

        public RefreshArtistService(IProvideArtistInfo artistInfo,
                                    IArtistService artistService,
                                    IArtistMetadataService artistMetadataService,
                                    IAlbumService albumService,
                                    IRefreshAlbumService refreshAlbumService,
                                    IEventAggregator eventAggregator,
                                    IMediaFileService mediaFileService,
                                    IHistoryService historyService,
                                    IDiskScanService diskScanService,
                                    ICheckIfArtistShouldBeRefreshed checkIfArtistShouldBeRefreshed,
                                    IConfigService configService,
                                    IImportListExclusionService importListExclusionService,
                                    Logger logger)
        : base(logger, artistMetadataService)
        {
            _artistInfo = artistInfo;
            _artistService = artistService;
            _albumService = albumService;
            _refreshAlbumService = refreshAlbumService;
            _eventAggregator = eventAggregator;
            _mediaFileService = mediaFileService;
            _historyService = historyService;
            _diskScanService = diskScanService;
            _checkIfArtistShouldBeRefreshed = checkIfArtistShouldBeRefreshed;
            _configService = configService;
            _importListExclusionService = importListExclusionService;
            _logger = logger;
        }
        
        protected override RemoteData GetRemoteData(Artist local, List<Artist> remote)
        {
            var result = new RemoteData();
            try
            {
                result.Entity = _artistInfo.GetArtistInfo(local.Metadata.Value.ForeignArtistId, local.MetadataProfileId);
                result.Metadata = new List<ArtistMetadata> { result.Entity.Metadata.Value };
            }
            catch (ArtistNotFoundException)
            {
                _logger.Error($"Could not find artist with id {local.Metadata.Value.ForeignArtistId}");
            }

            return result;
        }
                  
        protected override bool ShouldDelete(Artist local)
        {
            return !_mediaFileService.GetFilesByArtist(local.Id).Any();
        }
        
        protected override void LogProgress(Artist local)
        {
            _logger.ProgressInfo("Updating Info for {0}", local.Name);            
        }
            
        protected override bool IsMerge(Artist local, Artist remote)
        {
            return local.ArtistMetadataId != remote.Metadata.Value.Id;
        }

        protected override UpdateResult UpdateEntity(Artist local, Artist remote)
        {
            UpdateResult result = UpdateResult.None;
            
            if(!local.Metadata.Value.Equals(remote.Metadata.Value))
            {
                result = UpdateResult.UpdateTags;
            }

            local.UseMetadataFrom(remote);
            local.Metadata = remote.Metadata;
            local.LastInfoSync = DateTime.UtcNow;

            try
            {
                local.Path = new DirectoryInfo(local.Path).FullName;
                local.Path = local.Path.GetActualCasing();
            }
            catch (Exception e)
            {
                _logger.Warn(e, "Couldn't update artist path for " + local.Path);
            }

            return result;
        }
        
        protected override UpdateResult MoveEntity(Artist local, Artist remote)
        {
            _logger.Debug($"Updating MusicBrainz id for {local} to {remote}");

            // We are moving from one metadata to another (will already have been poplated)
            local.ArtistMetadataId = remote.Metadata.Value.Id;
            local.Metadata = remote.Metadata.Value;

            // Update list exclusion if one exists
            var importExclusion = _importListExclusionService.FindByForeignId(local.Metadata.Value.ForeignArtistId);

            if (importExclusion != null)
            {
                importExclusion.ForeignId = remote.Metadata.Value.ForeignArtistId;
                _importListExclusionService.Update(importExclusion);
            }

            // Do the standard update
            UpdateEntity(local, remote);
            
            // We know we need to update tags as artist id has changed
            return UpdateResult.UpdateTags;
        }

        protected override UpdateResult MergeEntity(Artist local, Artist target, Artist remote)
        {
            _logger.Warn($"Artist {local} was replaced with {remote} because the original was a duplicate.");

            // Update list exclusion if one exists
            var importExclusionLocal = _importListExclusionService.FindByForeignId(local.Metadata.Value.ForeignArtistId);

            if (importExclusionLocal != null)
            {
                var importExclusionTarget = _importListExclusionService.FindByForeignId(target.Metadata.Value.ForeignArtistId);
                if (importExclusionTarget == null)
                {
                    importExclusionLocal.ForeignId = remote.Metadata.Value.ForeignArtistId;
                    _importListExclusionService.Update(importExclusionLocal);
                }
            }

            // move any albums over to the new artist and remove the local artist
            var albums = _albumService.GetAlbumsByArtist(local.Id);
            albums.ForEach(x => x.ArtistMetadataId = target.ArtistMetadataId);
            _albumService.UpdateMany(albums);
            _artistService.DeleteArtist(local.Id, false);

            // Update history entries to new id
            var items = _historyService.GetByArtist(local.Id, null);
            items.ForEach(x => x.ArtistId = target.Id);
            _historyService.UpdateMany(items);

            // We know we need to update tags as artist id has changed
            return UpdateResult.UpdateTags;
        }

        protected override Artist GetEntityByForeignId(Artist local)
        {
            return _artistService.FindById(local.ForeignArtistId);
        }

        protected override void SaveEntity(Artist local)
        {
            _artistService.UpdateArtist(local);
        }

        protected override void DeleteEntity(Artist local, bool deleteFiles)
        {
            _artistService.DeleteArtist(local.Id, true);
        }

        protected override List<Album> GetRemoteChildren(Artist remote)
        {
            return remote.Albums.Value.DistinctBy(m => m.ForeignAlbumId).ToList();
        }

        protected override List<Album> GetLocalChildren(Artist entity, List<Album> remoteChildren)
        {
            return _albumService.GetAlbumsForRefresh(entity.ArtistMetadataId,
                                                     remoteChildren.Select(x => x.ForeignAlbumId)
                                                     .Concat(remoteChildren.SelectMany(x => x.OldForeignAlbumIds)));
        }
        
        protected override Tuple<Album, List<Album> > GetMatchingExistingChildren(List<Album> existingChildren, Album remote)
        {
            var existingChild = existingChildren.SingleOrDefault(x => x.ForeignAlbumId == remote.ForeignAlbumId);
            var mergeChildren = existingChildren.Where(x => remote.OldForeignAlbumIds.Contains(x.ForeignAlbumId)).ToList();
            return Tuple.Create(existingChild, mergeChildren);
        }
        
        protected override void PrepareNewChild(Album child, Artist entity)
        {
            child.Artist = entity;
            child.ArtistMetadata = entity.Metadata.Value;
            child.ArtistMetadataId = entity.Metadata.Value.Id;
            child.Added = DateTime.UtcNow;
            child.LastInfoSync = DateTime.MinValue;
            child.ProfileId = entity.QualityProfileId;
            child.Monitored = entity.Monitored;
        }
        
        protected override void PrepareExistingChild(Album local, Album remote, Artist entity)
        {
            local.Artist = entity;
            local.ArtistMetadata = entity.Metadata.Value;
            local.ArtistMetadataId = entity.Metadata.Value.Id;
        }
        
        protected override void AddChildren(List<Album> children)
        {
            _albumService.InsertMany(children);
        }

        protected override bool RefreshChildren(SortedChildren localChildren, List<Album> remoteChildren, bool forceChildRefresh, bool forceUpdateFileTags)
        {
            // we always want to end up refreshing the albums since we don't get have proper data
            Ensure.That(localChildren.UpToDate.Count, () => localChildren.UpToDate.Count).IsLessThanOrEqualTo(0);
            return _refreshAlbumService.RefreshAlbumInfo(localChildren.All, remoteChildren, forceChildRefresh, forceUpdateFileTags);
        }
            
        protected override void PublishEntityUpdatedEvent(Artist entity)
        {
            _eventAggregator.PublishEvent(new ArtistUpdatedEvent(entity));
        }

        protected override void PublishRefreshCompleteEvent(Artist entity)
        {
            _eventAggregator.PublishEvent(new ArtistRefreshCompleteEvent(entity));
        }

        protected override void PublishChildrenUpdatedEvent(Artist entity, List<Album> newChildren, List<Album> updateChildren)
        {
            _eventAggregator.PublishEvent(new AlbumInfoRefreshedEvent(entity, newChildren, updateChildren));
        }

        private void RescanArtist(Artist artist, bool isNew, CommandTrigger trigger, bool infoUpdated)
        {
            var rescanAfterRefresh = _configService.RescanAfterRefresh;
            var shouldRescan = true;

            if (isNew)
            {
                _logger.Trace("Forcing rescan of {0}. Reason: New artist", artist);
                shouldRescan = true;
            }

            else if (rescanAfterRefresh == RescanAfterRefreshType.Never)
            {
                _logger.Trace("Skipping rescan of {0}. Reason: never recan after refresh", artist);
                shouldRescan = false;
            }

            else if (rescanAfterRefresh == RescanAfterRefreshType.AfterManual && trigger != CommandTrigger.Manual)
            {
                _logger.Trace("Skipping rescan of {0}. Reason: not after automatic scans", artist);
                shouldRescan = false;
            }

            if (!shouldRescan)
            {
                return;
            }

            try
            {
                // If some metadata has been updated then rescan unmatched files.
                // Otherwise only scan files that haven't been seen before.
                var filter = infoUpdated ? FilterFilesType.Matched : FilterFilesType.Known;
                _diskScanService.Scan(artist, filter);
            }
            catch (Exception e)
            {
                _logger.Error(e, "Couldn't rescan artist {0}", artist);
            }
        }

        public void Execute(RefreshArtistCommand message)
        {
            var trigger = message.Trigger;
            var isNew = message.IsNewArtist;

            if (message.ArtistId.HasValue)
            {
                var artist = _artistService.GetArtist(message.ArtistId.Value);
                bool updated = false;
                try
                {
                    updated = RefreshEntityInfo(artist, null, true, false);
                    _logger.Trace($"Artist {artist} updated: {updated}");
                    RescanArtist(artist, isNew, trigger, updated);
                }
                catch (Exception e)
                {
                    _logger.Error(e, "Couldn't refresh info for {0}", artist);
                    RescanArtist(artist, isNew, trigger, updated);
                    throw;
                }
            }
            else
            {
                var allArtists = _artistService.GetAllArtists().OrderBy(c => c.Name).ToList();

                foreach (var artist in allArtists)
                {
                    var manualTrigger = message.Trigger == CommandTrigger.Manual;

                    if (manualTrigger || _checkIfArtistShouldBeRefreshed.ShouldRefresh(artist))
                    {
                        bool updated = false;
                        try
                        {
                            updated = RefreshEntityInfo(artist, null, manualTrigger, false);
                        }
                        catch (Exception e)
                        {
                            _logger.Error(e, "Couldn't refresh info for {0}", artist);
                        }

                        RescanArtist(artist, false, trigger, updated);
                    }
                    else
                    {
                        _logger.Info("Skipping refresh of artist: {0}", artist.Name);
                        RescanArtist(artist, false, trigger, false);
                    }
                }
            }
        }
    }
}
